"""
api.py

This file is part of a program that implements a Software-Defined Radio.

Copyright (C) 2014 by G3UKB Bob Cowdery

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

The authors can be reached by email at:

	bob@bobcowdery.plus.com

"""

# Python imports
import sys, os
from time import sleep
import numpy as np
import logging

# Allow for dev and runtime
sys.path.append('..\\..\\build\\windows\\bin')
sys.path.append('..\\..\\bin')

# Application imports
from common.defs import *
import common.instance_cache as instance_cache
import libsdrscript

"""
Interface to processing extension.

This module provides an abstraction layer to the extension library.
Although most calls are pass-through it allows for:
	o interface logic
	o isolates calling code from any changes that might be made in the extension
	o alternative implementations for backward compatibility
	o error management

"""

"""
AudioApi: Enumerate devices

Single instance
"""
class AudioApi:
	
	def __init__(self):
		self.__server = instance_cache.getInstance('cyserver')
	
	def enum_input_devices(self):
		return self.__server.enum_inputs()
		
	def enum_output_devices(self):
		return self.__server.enum_outputs()
	
"""
ControlApi: Main control functions

Single instance
"""
class ControlApi:
	
	def __init__(self, metis_sock, metis_address, METIS_PORT, q):
		"""
		Constructor
		
		Arguments:
			metis_sock		-- open socket to Metis
			metis_address	-- IP address of Metis
			METIS_PORT		-- Metis port
			q				-- Q to send message to
		
		"""
		
		self.__q = q
		
		self.__server = libsdrscript.Server(metis_sock, metis_address, METIS_PORT, q)
		# Add to cache so other api's can use it
		instance_cache.addToCache('cyserver', self.__server)
		self.__q.put ((BOTH, logging.INFO, 'py.ControlApi', 'Checking widom file...'))
		self.__server.make_wisdom()
	
	# =========================================================================================================
	# Management - start/stop/reset processing
	def start_processing(self, control_cfg, allocated_channels, allocated_displays, cc_callback, test_mode):
		"""
		Start pipeline processing
		
		Arguments:
			control_cfg			-- the current configuration as held by the control ui
			allocated_channels	-- {CH_TX: (rx_id, ch_object), ..., CH_RX: (rx_id, ch_object), ...}
			allocated_displays	-- {rx_id: unit_object, ...}
			cc_callback			-- Callback here with cc data
			test_mode			-- UI development, pass it on
		
		Configuration is of the form:	
		DEFAULT_CONFIG: {
			COMMON: {
				IN_RATE: 48000,
				OUT_RATE: 48000,
				NUM_RX: 1,
				IQ_BLK_SZ: 1024,
				MIC_BLK_SZ: 1024,
				DUPLEX: False,
				TX: False,
				POWER: {
					PWR_MODE: AUTO,
					PWR_IP: '192.168.1.100',
					PWR_USER: 'admin',
					PWR_PW: '12345678',
					PWR_OUTLET: 1,
				},
				AUDIO: {
					IN_SRC: HPSDR,
					IN_HOSTAPI: '',
					IN_DEV: '',
					OUT_SINK: HPSDR,
					ROUTING: {
						HPSDR:[[1, '', '', BOTH],],
						LOCAL: []   
					}
				},
				ANTENNA: {
					ANT_SETTING: {ANT_1: [MIN_FREQ, MAX_FREQ],},
					ANT_PREC: 'default'
				}
			},
			RX: {
				DISPLAYS: {
					PAN_MODE: 3,
					PAN_FRM_AV: 30,
					WINDOW_TYPE: 5,
					PAN_FFT_SZ: 4096
				}
			}
			TX: {
				LEVELS: {
					TUNE_DRIVE_LEVEL: 20
				}
			} 
		}
		
		"""
		
		# Convert the given channel structure into an array of TX and RX id against a channel number.
		# This is what the lower levels need to work with
		tx_chs = []
		for tx in allocated_channels[CH_TX]:
			tx_chs.append((tx, allocated_channels[CH_TX][tx].get_ch_id()))
		rx_chs = []
		for rx in allocated_channels[CH_RX]:
			rx_chs.append((rx, allocated_channels[CH_RX][rx].get_ch_id()))
		rx_disp = []
		for rx in allocated_displays:
			rx_disp.append((rx, allocated_displays[rx].get_display_id()))
		# Start server processing
		return self.__server.start_processing(control_cfg, tx_chs, rx_chs, rx_disp, cc_callback, test_mode)				
	
	def run_display(self, run_state):
		""" Toggle pipeline display processing """

		self.__server.run_display(run_state)
		
	def stop_processing(self):
		""" Stop pipeline processing """

		self.__server.stop_processing()
		
	def reset_processing(self):
		""" Restart pipeline processing """

		self.stop_processing()
		self.start_processing()	

"""
MetisApi: H/W configuration and control functions

Single Instance (at present as only one device is allowed)
"""
class MetisApi:
	
	# =========================================================================================================
	# HPSDR H/W - The H/W is configured with in-band signalling.
	
	def __init__(self, q):
		"""
		Constructor
		
		Arguments:
			q	-- Q to send message to
			
		"""
		
		self.__q = q
		
		self.__server = instance_cache.getInstance('cyserver')
								  
	def set_mox(self, value):
		"""
		Set MOX on/off
		
		Arguments:
			value -- True = TX
			valid = True | False
		
		"""
		
		self.__server.set_mox(value)

	def set_speed(self, value):
		"""
		Set the input sample rate
		
		Arguments:
			value -- integer speed value
			valid = 48, 96, 192, 384
		
		"""
		
		self.__server.set_speed(value)
	
	def set_10MHz_ref(self, value):
		"""
		Set the 10Mhz reference source
		
		Arguments:
			value -- name of source
			valid = 'external', 'penelope', 'mercury'
		
		"""
		
		self.__server.set_10MHz_ref(value)
	
	def set_122_88MHz_ref(self, value):
		"""
		Set the 122.88Mhz reference source
		
		Arguments:
			value -- name of source
			valid = 'penelope', 'mercury'
		
		"""
		
		self.__server.set_122_88MHz_ref(value)
	
	def set_board_config(self, value):
		"""
		Set the board configuration
		
		Arguments:
			value -- board mix
			valid = 'nil', 'penelope', 'mercury', 'both'
		
		"""
		
		self.__server.set_board_config(value)
	
	def set_mic_source(self, value):
		""" Set the mic source
		
		Arguments:
			value -- microphone source
			valid = 'janus', 'penelope'
		
		"""
		
		self.__server.set_mic_source(value)

	def set_class_mode(self, value):
		"""
		Set the class mode
		
		Arguments:
			value -- mode
			valid = 'mode_e', 'mode_other'
		
		"""
		
		self.__server.set_class_mode(value)
	
	def set_oc_outputs(self, value):
		"""
		Set the open collector outputs
		
		Arguments:
			value -- bit settings
		
		"""
		
		self.__server.set_oc_outputs(value)
	
	def set_alex_attn(self, value):
		"""
		Set Alex attenuator level
		
		Arguments:
			value -- db level
			valid = 0, 10, 20, 30
		
		"""
		
		self.__server.set_alex_attn(value)
	
	def set_preamp(self, value):
		"""
		Set Alex preamp
		
		Arguments:
			value -- True = on
			valid = True, False
		
		"""
		
		self.__server.set_preamp(value)
		
	def set_LT2208_dither(self, value):
		"""
		Set L2280 Dither
		
		Arguments:
			value -- True = on
			valid = True, False
			
		"""
		
		self.__server.set_LT2208_dither(value)

	def set_LT2208_random(self, value):
		"""
		Set L2280 Random
		
		Arguments:
			value -- True = on
			valid = True, False
		
		"""
		
		self.__server.set_LT2208_random(value)
		
	def set_alex_rx_ant(self, value):
		"""
		Select Alex rx antenna source
		
		Arguments:
			value -- antenna source
			valid = 'none', 'rx1', 'rx2', 'xv'
		
		"""
		
		self.__server.set_alex_rx_ant(value)
		
	def set_alex_rx_out(self, value):
		"""
		Set Alex RX out
		
		Arguments:
			value -- True = on
			valid = True, False
		
		"""
		
		self.__server.set_alex_rx_out(value)
		
	def set_alex_tx_relay(self, value):
		"""
		Set Alex TX relay output
		
		Arguments:
			value -- relay output
			valid = 'tx1', 'tx2', 'tx3'
		
		"""
		
		self.__server.set_alex_tx_relay(value)

	def set_alex_6m_preamp(self, value):
		"""
		Set Alex 6M preamp
		
		Arguments:
			value -- True = on
			valid = True, False
		
		"""
		
		self.__server.set_alex_6m_preamp(value)
	
	def set_duplex(self, value):
		"""
		Set simplex/duplex
		
		Arguments:
			value -- True = duplex
			valid = True, False
		
		"""
		
		self.__server.set_duplex(value)
		
	def set_number_rx(self, value):
		"""
		Set number of receivers
		
		Arguments:
			value -- number 1-8
			valid = 1,2,3,4,5,6,7,8
		
		"""
		
		self.__server.set_number_rx(value)
		
	def set_mic_ts(self, value):
		"""
		Set 1PPS timestamp on ic data
		
		Arguments:
			value -- True = on
			valid = True, False
		
		"""
		
		self.__server.set_mic_ts(value)
	
	def set_mercury_freq_plan(self, value):
		"""
		Set independent or common frequency for multiple mercury boards
		
		Arguments:
			value -- plan
			valid = 'independent', 'common'
		
		"""
		
		self.__server.set_mercury_freq_plan(value)
	
	def set_rx_freq(self, rx, freq):
		"""
		Set frequency for rx
		
		Arguments:
			rx 	-- rx number
			freq 	-- float format in MHz
		
		"""
		 
		# Fix this - rx is 1 based in cython land
		self.__server.set_rx_freq(rx+1, freq)
		
	def set_tx_freq(self, tx, freq):
		"""
		Set frequency for tx
		
		Arguments:
			tx 		-- tx number
			freq 	-- float format in MHz
		
		"""
		 
		# Fix this - tx is 1 based in cython land
		self.__server.set_tx_freq(tx+1, freq)

"""
DSPChannelApi: DSP configuration and functions

Multi instance - instance per DSP channel
"""
class DSPChannelApi:
	
	# =========================================================================================================	
	# DSP
	
	def __init__(self, ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown, q):
		"""
		Constructor
		
		Arguments:
			ch_type 	-- CH_RX | CH_TX
			ch_instance	-- The RX instance id (1-8) or TX instance id (1 only at present)
							that will be associated with this channel
			iq_size		-- 128, 256, 1024, 2048, 4096
			mic_sz		-- as iq_size for same sample rate
			in_rate		-- input sample rate
			out_rate	-- output sample rate
			tdelayup	-- delay before up slew
			tslewup		-- length of up slew
			tdelaydown	-- delay before down slew
			tslewdown	-- length of down slew
			q			-- Q to send message to
			
		"""
		
		self.__ch_type = ch_type
		self.__ch_instance = ch_instance
		self.__iq_size = iq_size
		self.__mic_size = mic_size
		self.__in_rate = in_rate
		self.__out_rate = out_rate
		self.__tdelayup = tdelayup
		self.__ctslewup = tslewup
		self.__tdelaydown = tdelaydown
		self.__tslewdown = tslewdown
		self.__ch_id = None
		
		# Get the server instance
		self.__server = instance_cache.getInstance('cyserver')
								  
		# Open the channel and stash the channel id
		self.__ch_id = self.__open_channel(ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown)
		
	def close_channel(self):
		""" Close this channel """
		
		if self.__ch_id != None:
			self.__close_channel(self.__ch_id)
			self.__ch_id = None

	def __del__(self):
		""" Destructor """
		
		# Make sure the channel gets closed if the caller forgot
		if self.__ch_id != None:
			self.__close_channel(self.__ch_id)
		
	#======================================================
	# Basic channel operations
	
	def get_ch_id(self):
		""" Return the channel id for this channel """
		
		return self.__ch_id
	
	def set_ch_state(self, state, mode):
		"""
		Change the channel state 
		
		Arguments:
			state		-- CH_STATE_STOP | CH_STATE_START
			mode		-- CH_TRANSITION_WAIT | CH_TRANSITION_NOWAIT
			
		"""
		
		return self.__server.set_ch_state(self.__ch_id, state, mode)
	
	def set_dsp_sz(self, sz):
		"""
		Set the DSP block size in complex samples 
		
		Arguments:
			sz 		-- block size
		
		"""
		
		self.__server.set_dsp_sz(self.__ch_id, sz)
		
	def set_tdelayup(self, delay):
		"""
		Change the delay up time in seconds 
		
		Arguments:
			delay 	-- delay in seconds
		
		"""
		
		self.__server.set_tdelayup(self.__ch_id, delay)
		
	def set_tslewup(self, slew):
		"""
		Change the slew up time in seconds 
		
		Arguments:
			slew 	-- slew up time in seconds
		
		"""
		
		self.__server.set_tslewup(self.__ch_id, slew)
		
	def set_tdelaydown(self, delay):
		"""
		Change the delay up time in seconds 
		
		Arguments:
			delay 	-- delay in seconds
		
		"""
		
		self.__server.set_tdelaydown(self.__ch_id, delay)
		
	def set_tslewdown(self, slew):
		"""
		Change the slew down time in seconds 
		
		Arguments:
			slew 	-- slew down time in seconds
		
		"""
		
		self.__server.set_tslewdown(self.__ch_id, slew)
	
	#======================================================
	# Receiver type channel parameters
	def set_rx_mode(self, mode):
		
		"""
		Set the receiver mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode 	-- CH_LSB | CH_USB |CH_DSB |CH_CWL |CH_CWU |CH_FM |CH_AM |CH_DIGU |CH_SPEC |CH_DIGL |CH_SAM | CH_DRM
		
		"""
		
		self.__server.set_rx_mode(self.__ch_id, mode)
	
	def set_rx_filter_run(self, run):
		
		"""
		Set the receiver bandpass run mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			run  	-- CH_BP_OFF | CH_BP_ON
		
		"""
		
		self.__server.set_rx_filter_run(self.__ch_id, run)
	
	def set_rx_filter_freq(self, low, high):
		
		"""
		Set the receiver bandpass frequencies on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			low  	-- low cutoff in Hz
			high	-- high cutoff in Hz
			
		Note: frequencies can be +ve or _ve but low masy be numerically less that high
		
		"""
		
		self.__server.set_rx_filter_freq(self.__ch_id, low, high)
	
	def set_rx_filter_window(self, window):
		
		"""
		Set the receiver bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			window  -- CH_BP_WIN_4_TERM | CH_BP_WIN_7_TERM
			
		Note: Window is Blackman-Harris 4 or 7 term.
		
		"""
		
		self.__server.set_rx_filter_window(self.__ch_id, window)
	
	def set_agc_mode(self, mode):
		
		"""
		Set the receiver bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode  -- CH_AGC_OFF | CH_AGC_LONG | CH_AGC_SLOW | CH_AGC_MED | CH_AGC_FAST
			
		Note: All defaults apply at this time.
		
		"""
		
		self.__server.set_agc_mode(self.__ch_id, mode)

	def get_rx_meter_data(self, which):
		
		"""
		Get the requested meter data for the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			which  -- CH_S_PEAK | CH_S_AV | CH_IN_PEAK | CH_IN_AV | CH_AGC_GAIN | CH_AGC_PEAK | CH_AGC_AV
			
		Returns: reading in db
		
		"""
		
		return self.__server.get_rx_meter_data(self.__ch_id, which)
	
	def set_rx_gain(self, gain):
		
		"""
		Set the rx output gain
		
		Arguments:
			gain  	-- the gain value 0.0 - 1.0
			
		"""
	
		self.__server.set_rx_gain(self.__ch_instance, gain)
		
	#======================================================
	# Transmitter type channel parameters
	def set_tx_mode(self, mode):
		
		"""
		Set the transmitter mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode 	-- CH_LSB | CH_USB |CH_DSB |CH_CWL |CH_CWU |CH_FM |CH_AM |CH_DIGU |CH_SPEC |CH_DIGL |CH_SAM | CH_DRM
		
		"""
		
		self.__server.set_tx_mode(self.__ch_id, mode)
	
	def set_tx_filter_run(self, run):
		
		"""
		Set the transmitter bandpass run mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			run  	-- CH_BP_OFF | CH_BP_ON
		
		"""
		
		self.__server.set_tx_filter_run(self.__ch_id, run)
	
	def set_tx_filter_freq(self, low, high):
		
		"""
		Set the transmitter bandpass frequencies on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			low  	-- low cutoff in Hz
			high	-- high cutoff in Hz
			
		Note: frequencies can be +ve or _ve but low masy be numerically less that high
		
		"""
		
		self.__server.set_tx_filter_freq(self.__ch_id, low, high)
	
	def set_tx_filter_window(self, window):
		
		"""
		Set the transmitter bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			window  -- CH_BP_WIN_4_TERM | CH_BP_WIN_7_TERM
			
		Note: Window is Blackman-Harris 4 or 7 term.
		
		"""
		
		self.__server.set_tx_filter_window(self.__ch_id, window)
	
	def get_tx_meter_data(self, which):
		
		"""
		Get the requested tx meter data for the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			which  -- M_TX_MIC_PK | M_TX_MIV_AV | M_TX_EQ_PK | M_TX_EQ_AV | M_TX_LEV_PK | M_TX_LEV_AV | M_TX_LEV_GAIN | M_TX_COMP_PK | M_TX_COMP_AV | M_TX_ALC_PK | M_TX_ALC_AV | M_TX_ALC_GAIN | M_TX_OUT_PK | M_TX_OUT_AV
			
		Returns: reading in db
		
		"""
		
		return self.__server.get_tx_meter_data(self.__ch_id, which)
	
	def set_tx_gain(self, gain):
		
		"""
		Set the tx output gain
		
		Arguments:
			gain  	-- the gain value 0.0 - 1.0
			
		"""
	
		self.__server.set_tx_gain(gain)
	
	# ====================================================================================	
	# PRIVATE
	
	def __open_channel(self, ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown):
		"""
		Open a new DSP channel 
		
		Arguments:
			ch_type 	-- CH_RX | CH_TX
			ch_instance	-- The RX instance id (1-8) or TX instance id (1 only at present)
							that will be associated with this channel
			iq_size		-- 128, 256, 1024, 2048, 4096
			mic_size	-- at same rate same as iq_size
			in_rate		-- input sample rate
			out_rate	-- output sample rate
			tdelayup	-- delay befor up slew
			tslewup		-- length of up slew
			tdelaydown	-- delay before down slew
			tslewdown	-- length of down slew
			
		Returns:
			the assigned channel number
		
		Note:
			There are additional parameters to open_channel. These are handled as follows:
				o in_size - the number of samples supplied to the channel. Same as dsp_size.
				o input_samplerate - taken from the set_speed() API call, default 48K.
				o dsp_rate - same as input_samplerate.
				o output_samplerate - fixed at 48K for RX TBD TX
				
		The channel is not automatically started. Call set_ch_state() to start the channel.
			
		"""
		
		r = self.__server.open_channel(ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown)
		return r
	
	def __close_channel(self, channel):
		"""
		Close an open channel 
		
		Arguments:
			channel 	-- the channel id as returned by open_channel()
			
		"""
		
		return self.__server.close_channel(channel)
	
	
"""
DisplayUnitApi: Display configuration and functions

Multi instance - instance per display unit
"""
class DisplayUnitApi:
	
	# =========================================================================================================	
	# Display Unit
	
	def __init__(self, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate, period, callback, q):
		"""
		Constructor
		
		Arguments:
			fft_size		-- fft size to use, power of 2
			win_type		-- window type
			sub_spans		-- number of receivers to stitch
			in_sz			-- number of input samples
			display_width	-- number of points to plot, generally pixel width
			average_mode	-- modes available :
								-1	Peak detect
								0	No averaging
								1	Time weighted linear
								2	Time weighted log
								3	Window averaging linear
								4	Window averaging log
								5	Weighted averaging linear, low noise floor mode
								6	Weighted averaging log, low noise floor mode
			over_frames		--	number of frames to average over
			sample_rate		-- 	in Hz
			period			--	minimum period in ms between refreah callbacks
			callback		--	callback here with display data every period ms
			q				-- Q to send message to
		
		Only necessary parameters are collected, in the light of experience further parameters
		may be added later.
		
		"""
		
		# Get the server instance
		self.__server = instance_cache.getInstance('cyserver')
								  
		# Create the display instance
		self.__display_id = self.__server.open_display(fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate, period, callback)
	
	def set_display(self, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate):
		"""
		Constructor
		
		Arguments:	
			fft_size		-- fft size to use, power of 2
			win_type		-- window type
			sub_spans		-- number of receivers to stitch
			in_sz			-- number of input samples
			display_width	-- number of points to plot, generally pixel width
			average_mode	-- modes available :
								-1	Peak detect
								0	No averaging
								1	Time weighted linear
								2	Time weighted log
								3	Window averaging linear
								4	Window averaging log
								5	Weighted averaging linear, low noise floor mode
								6	Weighted averaging log, low noise floor mode
			over_frames		--	number of frames to average over
			sample_rate		-- 	in Hz
		
		Only necessary parameters are collected, in the light of experience further parameters
		may be added later.
		
		"""
		
		# Get the server instance		
		self.__server = instance_cache.getInstance('cyserver')						  
		# Set the display instance parameters
		self.__server.set_display(self.__display_id, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate)
		
	def close_display(self):
		""" Close this display unit """
		
		if self.__display_id != None:
			self.__server.close_display(self.__display_id)
			self.__display_id = None

	def __del__(self):
		""" Destructor """
		
		# Make sure the display gets closed if the caller forgot
		if self.__display_id != None:
			self.__close_display(self.__display_id)
		
	# ======================================================
	# Basic display operations
	
	def get_display_id(self):
		""" Return the display id for this display """
		
		return self.__display_id
	
	